#region Using References

using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Globalization;
using System.IO;
using System.Linq;
using Beaker.OpenCube.Game;
using Beaker.OpenCube.Mapping;
using Beaker.OpenCube.Nbt;
using Beaker.Services.FileSystem;

#endregion

namespace Beaker.OpenCube.Dynamic
{
	/// <summary>
	/// Class that process data for a level of the game.
	/// </summary>
	public sealed class DynamicWorld
	{
		private readonly DynamicGame _owner;
		private readonly IFolder _worldFolder;

		/// <summary>
		/// Creates a new level reader for reading game level information.
		/// </summary>
		/// <param name="owner">The <see cref="DynamicGame"/> used to open this level reader.</param>
		/// <param name="worldFolder"></param>
		internal DynamicWorld( DynamicGame owner, IFolder worldFolder )
		{
			Contract.Requires<ArgumentNullException>( owner != null );
			Contract.Requires<ArgumentNullException>( worldFolder != null );

			_owner = owner;
			_worldFolder = worldFolder;
		}

		[ContractInvariantMethod]
		private void ContractInvariant()
		{
			Contract.Invariant( _owner != null );
			Contract.Invariant( _worldFolder != null );
		}

		/// <summary>
		/// Gets the folder of the world.
		/// </summary>
		/// <value>
		/// The world folder.
		/// </value>
		public IFolder WorldFolder
		{
			get
			{
				Contract.Ensures( Contract.Result<IFolder>() != null );
				return _worldFolder;
			}
		}

		private readonly WeakReference<LevelData> _levelData = new WeakReference<LevelData>( null );

		/// <summary>
		/// Gets the world data.
		/// </summary>
		/// <value>
		/// The world data.
		/// </value>
		public LevelData LevelData
		{
			get
			{
				return _levelData.GetOrCreate( ReadLevelData );
			}
		}

		/// <summary>
		/// Reads the level data.
		/// </summary>
		/// <returns>The level data.</returns>
		private LevelData ReadLevelData()
		{
			// path to level.dat
			IFile levelDataFile = _worldFolder.GetFile( "level.dat" );

			// open leve.dat
			using( Stream stream = levelDataFile.OpenRead() )
			{
				// deserialize nbt file
				NbtDictionary dictionary = NbtReader.Deserialize( stream );
				if( dictionary != null )
				{
					// level.dat contains root Data node
					var data = dictionary["Data"] as IDictionary<string, object>;
					if( data != null )
					{
						// create a mapper that maps to LevelData
						var mapper = new Mapper<LevelData>();
						// map and return
						return mapper.MapDictionary( data );
					}
				}
			}

			return null;
		}

		/// <summary>
		/// Gets a list of all the known names of players that are known for this level.
		/// </summary>
		/// <returns>A sequence of player names</returns>
		public IEnumerable<string> GetPlayerNames()
		{
			// player information resides in 'players' folder
			IFolder playersFolder = _worldFolder.GetFolder( "players" );
			if( playersFolder != null )
			{
				// this folder should contain a *.dat file for every player
				IEnumerable<IFile> datFiles = playersFolder
					.GetFiles()
					.Where( f => f.Name.EndsWith( ".dat", true, CultureInfo.InvariantCulture ) );

				foreach( IFile datFile in datFiles )
					yield return datFile.GetFileNameWithoutExtension();
			}
		}

		/// <summary>
		/// Get the player data for the player with the given name.
		/// </summary>
		/// <param name="playerName">The name of the player to get the player data for.</param>
		/// <returns>
		/// The player data for the player with the given name.
		/// </returns>
		/// <exception cref="System.ArgumentOutOfRangeException">playerName does not resolve to player data.</exception>
		public PlayerData GetPlayerData( string playerName )
		{
			Contract.Requires<ArgumentNullException>( !string.IsNullOrEmpty( playerName ) );
			Contract.Ensures( Contract.Result<PlayerData>() != null );

			// player information resides in 'players' folder
			IFolder playersFolder = _worldFolder.GetFolder( "players" );
			if( playersFolder != null )
			{
				// this folder should contain a *.dat file for requested player
				IFile datFile = playersFolder.GetFile( playerName + ".dat" );
				if( datFile != null )
				{
					// open player file
					using( Stream stream = datFile.OpenRead() )
					{
						// deserialize and map
						NbtDictionary dictionary = NbtReader.Deserialize( stream );
						Mapper<PlayerData> mapper = Mapper.Create<PlayerData>();
						return mapper.MapDictionary( dictionary["Data"] as IDictionary<string, object> );
					}
				}
			}

			throw new ArgumentOutOfRangeException( "playerName" );
		}


		/// <summary>
		/// Gets the available dimensions in this level.
		/// </summary>
		/// <returns>Sequence of all the available dimensions in this level.</returns>
		public IEnumerable<Dimension> GetDimensions()
		{
			// Rules:
			// * Dimension folder not there, dimension not available
			// * Dimension folder there, check content for generated content, none, 
			//	then dimension not available in current save.

			// overworld always available
			yield return Dimension.Overworld;

			// now scan folder for dim folders
			IEnumerable<IFolder> folders =
				_worldFolder.GetFolders()
					.Where( f => f.Name.StartsWith( "DIM" ) );

			foreach( IFolder folder in folders )
			{
				// now check per DIM folder if there is data
				var reader = new DynamicDimension( folder );
				if( reader.HasChunks )
				{
					// when chunck data, determine dimension based on name
					int dimValue;
					if( int.TryParse( folder.Name, NumberStyles.Integer, CultureInfo.InvariantCulture, out dimValue ) )
					{
						// we have a int, ignore if value is inside Dimenion Enum, 
						// so we won't error on new version of the game that adds a new dimension
						yield return (Dimension)dimValue;
					}
				}
			}
		}

		/// <summary>
		/// Opens the given dimension.
		/// </summary>
		/// <param name="dimensionToOpen">The dimension to open.</param>
		/// <returns>The dynamic dimension reader for the given dimension.</returns>
		public DynamicDimension OpenDimension( Dimension dimensionToOpen )
		{
			return new DynamicDimension(
				_worldFolder.GetFolder( dimensionToOpen.ToFolderName() )
			);
		}
	}
}